// ----------------------------------------------------------------------------
// Copyright 2006, Martin D. Flynn
// All rights reserved
// ----------------------------------------------------------------------------
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// ----------------------------------------------------------------------------
// Description:
//  This is the main entry point for the midlet application.  This class handles
//  the initialization of the supporting components of the DMTP communication,
//  as well as handle the mail loop used to periodically check for changes in
//  GPS and time events.
// ----------------------------------------------------------------------------
// Change History:
//  2006/03/26  Martin D. Flynn
//      Initial release
//  2006/03/31  Martin D. Flynn
//      Placed in custom sub-pakage to allow device dependent UI packaging
// ----------------------------------------------------------------------------
package org.opendmtp.j2me.client.custom.treo650;

import java.util.Enumeration;

import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

import org.opendmtp.j2me.client.base.GPSModules;
import org.opendmtp.j2me.client.base.PersistentStorage;
import org.opendmtp.j2me.client.base.Props;
import org.opendmtp.j2me.client.base.Protocol;
import org.opendmtp.j2me.client.base.TimeModules;
import org.opendmtp.j2me.client.gps.GPSReceiver;
import org.opendmtp.j2me.client.modules.MotionModule;
import org.opendmtp.j2me.client.modules.TimeModule;
import org.opendmtp.j2me.util.CThread;
import org.opendmtp.j2me.util.DateTime;
import org.opendmtp.j2me.util.GeoEvent;
import org.opendmtp.j2me.util.KeyValue;
import org.opendmtp.j2me.util.Log;

/**
 * This is the main entry point for the MIDlet application. This class handles the initialization of
 * the supporting components of the DMTP communication, as well as handling the main loop used to
 * periodically check for changes in GPS and time events.
 * 
 * @author Martin D. Flynn.
 * @author Pavel V. Senin.
 * 
 */
public class Main extends MIDlet implements Runnable {

  /**
   * Defines log name constant.
   */
  private static final String LOG_NAME = "MAIN";

  /**
   * Defines copyright constant.
   */
  public static final String COPYRIGHT = "Copyright 2006, Martin D. Flynn";

  /**
   * Defines application version.
   */
  public static final String DMTP_VERS = "1.1.4";
  /**
   * Defines application name.
   */
  private static final String DMTP_NAME = "OpenDMTP";
  /**
   * Defines application type.
   */
  private static final String DMTP_TYPE = "J2ME";
  /**
   * Defines full application title.
   */
  public static final String DMTP_VERSION = DMTP_NAME + "_" + DMTP_TYPE + "." + DMTP_VERS;
  /**
   * Enable events constant.
   */
  private static final boolean ENABLE_EVENTS = true;
  /**
   * Defines time sample interval.
   */
  private static final long TIME_SAMPLE_INTERVAL = 60L; // seconds
  /**
   * Defines loop delay time.
   */
  private static final long STANDARD_LOOP_DELAY = 2000L; // millis
  /**
   * Defines fast loop delay time, WARNING: this field is never used.
   */
  private static final long FAST_LOOP_DELAY = 20L; // millis
  /**
   * Defines loop delay increment.
   */
  private static final long LOOP_DELAY_INCREMENT = 30L; // millis
  /**
   * Holds this implementation.
   */
  private static Main DMTP_Main = null;

  /**
   * Returns instance of this application. If no application exists creates new one.
   * 
   * @return current instance.
   */
  public static Main getInstance() {
    if (DMTP_Main == null) {
      DMTP_Main = new Main();
    }
    return DMTP_Main;
  }

  /**
   * Returns display associated with this application.
   * 
   * @return display instance.
   */
  public static Display getDisplay() {
    return Display.getDisplay(Main.getInstance());
  }

  /**
   * Returns a GPSDisplay instance associated with this application.
   * 
   * @return a GPSDisplay instance.
   */
  public static Displayable getGPSDisplayable() {
    return GPSDisplay.getInstance();
  }

  /**
   * Requests display of displayable object.
   * 
   * @param disp object to show.
   */
  public static void showDisplayable(Displayable disp) {
    Main.getDisplay().setCurrent(disp);
  }

  /**
   * Requests display of GPSDisplay associated with this application.
   * 
   */
  public static void showGPSDisplayable() {
    try {
      Main.showDisplayable(Main.getGPSDisplayable());
    }
    catch (Throwable th) {
      Log.error(LOG_NAME, "Show default Displayable", th);
    }
  }

  /**
   * Defines persistent storage.
   */
  private PersistentStorage propsStore = null;
  private boolean startupInit = false;
  private Displayable savedDisplay = null;
  private CThread mainLoopThread = null;

  private Protocol protocol = null;

  private GPSReceiver gpsReceiver = null;
  private GeoEvent gpsEvent = null;
  private GPSModules gpsModules = null;
  private TimeModules timeModules = null;

  private long gpsStaleTimer = 0L;
  private long lastGPSAquisitionTimer = 0L;
  private long lastTimeEventTimer = 0L;
  private GeoEvent lastValidGPSFix = new GeoEvent();
  private long loopDelayMS = STANDARD_LOOP_DELAY;

  /**
   * Creates and register MIDlet application within the system and associates it with this
   * implementation.
   * 
   */
  public Main() {
    super();
    DMTP_Main = this;
  }

  /**
   * Starts application. Initializes all properties and starts GPS module as well.
   * 
   * @throws MIDletStateChangeException when error encountered.
   */
  public void startApp() throws MIDletStateChangeException {
    if (!this.startupInit) {
      // new execution ...
      Log.info(LOG_NAME, "vvvvv Application STARTED vvvvv");
      this.startupInit = true;

      /* init properties */
      try {
        Props.initProps();
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "Property init error", th);
      }

      /* custom properties (set as default) */
      try {
        Props.initFromString(Props.PROP_COMM_DMTP_HOST, "", true);
        Props.initFromString(Props.PROP_COMM_DMTP_PORT, "31000", true);
        Props.initFromString(Props.PROP_STATE_ACCOUNT_ID, "opendmtp", true);
        Props.initFromString(Props.PROP_STATE_DEVICE_ID, "mobile", true);
        Props.initFromString(Props.PROP_COMM_MAX_CONNECTIONS, "15,15,30", true);
        Props.initFromString(Props.PROP_COMM_MIN_XMIT_DELAY, "60", true); // seconds
        Props.initFromString(Props.PROP_COMM_MIN_XMIT_RATE, "90", true); // seconds
        Props.initFromString(Props.PROP_MOTION_START, "10.0", true); // kph
        Props.initFromString(Props.PROP_MOTION_IN_MOTION, "240", true); // seconds
        Props.initFromString(Props.PROP_MOTION_STOP, "360", true); // seconds
        Props.initFromString(Props.PROP_MOTION_DORMANT_INTRVL, "3600", true); // seconds
        Props.initFromString(Props.PROP_MOTION_DORMANT_COUNT, "0", true);
        Props.initFromString(Props.PROP_MOTION_EXCESS_SPEED, "0.0", true); // kph
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "Custom property init error", th);
      }

      /* init from exteral JAD file properties. [this doesn't seem to work on the TREO] */
      try {
        for (Enumeration kvEnum = Props.getKeyValues(); kvEnum.hasMoreElements();) {
          KeyValue kv = (KeyValue) kvEnum.nextElement();
          String userKey = kv.getKeyName();
          String userVal = this.getAppProperty(userKey); // try name 1st
          if (userVal == null) {
            userKey = kv.getKeyCodeString();
            userVal = this.getAppProperty(userKey); // try code 2nd
          }
          if (userVal != null) {
            Log.debug(LOG_NAME, "Found User props: '" + userKey + "' ==> '" + userVal + "'");
            kv.initFromString(userVal, true); // set as default
          }
          else {
            // Log.warn(LOG_NAME, "User prop not found: '" + kv.getKeyName() + "'");
          }
        }
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "JAD property init error", th);
      }

      /* init from persistent storage */
      this.propsStore = new PersistentStorage("OpenDMTP");
      try {
        Props.loadFromStore(this.propsStore);
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "PersistentStorage init error", th);
      }

      /* header */
      String firmware = Props.getString(Props.PROP_STATE_FIRMWARE, "");
      String serial = Props.getString(Props.PROP_STATE_SERIAL, "");
      Log.println(LOG_NAME, "OpenDMTP protocol reference implementation.");
      Log.println(LOG_NAME, "Version: " + DMTP_VERSION + " [" + serial + "]");
      Log.println(LOG_NAME, COPYRIGHT);

      /* init display */
      try {
        Main.showGPSDisplayable();
        // try { Thread.sleep(1000L); } catch (Throwable t) {}
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "Init error", th);
      }

      /* check host:port */
      String hostStr = Props.getString(Props.PROP_COMM_DMTP_HOST, "");
      int portNum = (int) Props.getLong(Props.PROP_COMM_DMTP_PORT, 0, 0L);
      if (!OptionsScreen.isValidHost(hostStr) || (portNum <= 0) || (portNum > 0xFFFF)) {
        OptionsScreen.show();
      }

      /* OnScreen message handler */
      Log.setMessageHandler(new Log.LogMessageHandler() {
        public void setMessage(int ndx, String msg) {
          GPSDisplay.setMessage(ndx, msg);
        }
      });
      Log.setMessage(1, "Initializing ...");

      /* init gps receiver */
      try {
        GPSReceiver.init(new GPSDeviceImpl());
        this.gpsReceiver = GPSReceiver.getInstance();
        this.gpsEvent = new GeoEvent();
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "GPS init error", th);
      }

      /* protocol handler */
      try {
        this.protocol = Protocol.createInstance(new TransportImpl());
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "Protocol init error", th);
      }

      /* gps event modules */
      try {
        this.gpsModules = new GPSModules();
        this.gpsModules.addModule(new MotionModule(this.protocol.getEventQueue()));
        // add other modules here
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "GPS modules init error", th);
      }

      /* time event modules */
      try {
        this.timeModules = new TimeModules();
        this.timeModules.addModule(new TimeModule(this.protocol.getEventQueue()));
        // add other modules here
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "Time modules init error", th);
      }

      /* main loop thread */
      try {
        this.mainLoopThread = new CThread("Main", this);
      }
      catch (Throwable th) {
        Log.error(LOG_NAME, "Loog init error", th);
      }

      /* start threads */
      Log.debug(LOG_NAME, "Starting threads ...");
      CThread.startThreads();
      Log.setMessage(1, "Running ...");

      /* start as paused */
      // this.pauseApp();
      // super.notifyPaused();
    }
    else {
      // already initialized
      Log.info(LOG_NAME, "**** Application RESTARTED ...");

      /* message */
      Log.setMessage(1, "Restarting ...");

      /* restart threads */
      CThread.startThreads();

      /* restore display */
      if (this.savedDisplay != null) {
        Main.getDisplay().setCurrent(this.savedDisplay);
        this.savedDisplay = null;
      }

    }

  }

  /**
   * Pauses application.
   */
  public void pauseApp() {
    // save current display
    this.savedDisplay = Main.getDisplay().getCurrent();
    Log.info(LOG_NAME, "**** Application PAUSED ...");
  }

  /**
   * Destroys application. During the process all properties saved into storage.
   * 
   * 
   * @param unconditional if true no exception thrown. If false throws MIDletStateChangeException
   *        exception.
   * @throws MIDletStateChangeException if specified by parameter.
   */

  public void destroyApp(boolean unconditional) throws MIDletStateChangeException {
    if (!unconditional) {
      throw new MIDletStateChangeException();
    }
    CThread.stopThreads();
    this.saveProps();
    Log.info(LOG_NAME, "^^^^^ Application DESTROYED ^^^^^");
    super.notifyDestroyed();
  }

  /**
   * Exits from application.
   * 
   */

  public void exitApp() {
    // save current display
    try {
      this.destroyApp(true);
    }
    catch (MIDletStateChangeException msce) {
      // this won't occur
    }
  }

  /**
   * Loads properties from storage.
   * 
   */
  public void loadProps() {
    Props.loadFromStore(this.propsStore);
  }

  /**
   * Saves all properties into storage.
   * 
   */
  public void saveProps() {
    Props.saveToStore(this.propsStore, false);
  }

  /**
   * Runs application.
   */

  public void run() {
    while (!this.mainLoopThread.shouldStop()) {
      try {

        // -----------------
        // aquire GPS
        long gpsInterval = Props.getLong(Props.PROP_GPS_SAMPLE_RATE, 0, 15L);
        if (DateTime.isTimerExpired(this.lastGPSAquisitionTimer, gpsInterval)) {
          this.gpsAquire();
        }

        // -----------------
        // time based events
        if (ENABLE_EVENTS && DateTime.isTimerExpired(this.lastTimeEventTimer, TIME_SAMPLE_INTERVAL)) {
          this.timeModules.checkTime(DateTime.getCurrentTimeSec());
          this.lastTimeEventTimer = DateTime.getTimerSec();
        }

        // -----------------
        // time to transmit? (we have data and/or minimum times have expired)
        if (ENABLE_EVENTS) {
          this.protocol.transport();
        }

        // -----------------
        // short loop delay
        try {
          Thread.sleep(this.loopDelayMS);
        }
        catch (Throwable t) {/* ignore */
        }
        if (this.loopDelayMS < STANDARD_LOOP_DELAY) {
          // this is used for temporary speed-up/slow-down of the loop delay
          this.loopDelayMS += LOOP_DELAY_INCREMENT;
          if (this.loopDelayMS > STANDARD_LOOP_DELAY) {
            this.loopDelayMS = STANDARD_LOOP_DELAY;
          }
        }

      }
      catch (Throwable t) {
        if (this.mainLoopThread.shouldStop()) {
          break;
        }
        else {
          Log.error(LOG_NAME, "RunLoop error", t);
          try {
            Thread.sleep(2000L);
          }
          catch (Throwable th) {
          }
        }
      }

    } // while (!this.mainLoopThread.shouldStop())
  }

  /**
   * Aquire GPS module fix.
   * 
   */
  private void gpsAquire() {
    // Log.info(LOG_NAME, "Aquiring GPS fix ...");

    /* aquire GPS fix */
    long gpsAquireTimeout = Props.getLong(Props.PROP_GPS_AQUIRE_WAIT, 0, 0L);
    GeoEvent gps = this.gpsReceiver.aquire(gpsAquireTimeout, this.gpsEvent);
    // if ((gps == null) || !gps.isValid()) {
    // //Log.warn(LOG_NAME, "GPS fix is invalid");
    // } else
    // if (this.lastValidGPSFix.getTimestamp() == gps.getTimestamp()) {
    // Log.warn(LOG_NAME, "GPS fix timestamp didn't change: " + gps.getTimestamp());
    // }

    /* new valid fix? */
    if ((gps != null) && gps.isValid()
        && (this.lastValidGPSFix.getTimestamp() != gps.getTimestamp())) {
      Log.setMessage(0, "New GPS Fix [" + (new DateTime()).getTimeString() + "]");
      // Log.info(LOG_NAME, "New GPS fix ...");
      // We've received a new valid GPS fix
      // NOTES:
      // - PROP_GPS_ACCURACY is not currently taken into account
      // - It may be desirable to set the current system time to this GPS time.
      double minSpeedKPH = Props.getDouble(Props.PROP_GPS_MIN_SPEED, 0, 7.0);
      gps.checkMinimumSpeed(minSpeedKPH);
      this.lastGPSAquisitionTimer = DateTime.getTimerSec();
      if (ENABLE_EVENTS) {
        this.gpsModules.checkGPS(this.lastValidGPSFix, gps);
      }
      gps.copyTo(this.lastValidGPSFix);
      if (GPSReceiver.isGpsStale()) {
        // GPS was stale, but is no longer stale
        Log.debug(LOG_NAME, "GPS fix is now up to date ...");
        GPSReceiver.setGpsStale(false);
        this.gpsStaleTimer = 0L;
      }
      else {
        // still not stale
      }
      GPSDisplay.getInstance().updateValues(this.lastValidGPSFix);
    }
    else

    /* now stale? */
    if (!GPSReceiver.isGpsStale()) {
      // We've not received a valid GPS fix, however the last GPS fix (if any)
      // is not yet considered "stale".
      long gpsExpireInterval = Props.getLong(Props.PROP_GPS_EXPIRATION, 0, 360L);
      if (gpsExpireInterval <= 0L) {
        // The GPS fix is never considered "stale"
      }
      else if (this.lastValidGPSFix.isValid()) {
        // We have previously received at least 1 valid GPS fix. Set the timer to
        // the last valid fix, and compare the age of the fix to the GPS expiration
        // interval.
        this.gpsStaleTimer = DateTime.getTimerSec(this.lastValidGPSFix.getTimestamp());
        if (DateTime.isTimerExpired(this.gpsStaleTimer, gpsExpireInterval)) {
          // The timer has expired, we're now "stale"
          // Likely causes: (most likely to least likely)
          // 1) GPS antenna is obstructed (garage, building, etc.)
          // 2) GPS antenna has been disconnected/damaged.
          // 3) GPS receiver communication link has been disconnected.
          // 4) GPS receiver has become defective.
          // The last 2 can be ruled out by checking to see if we've received anything
          // at all from the GPS receiver, even an invalid (type 'V') record.
          GPSReceiver.setGpsStale(true);
        }
      }
      else if (this.gpsStaleTimer <= 0L) {
        // We've never received a valid GPS fix, and this is our first invalid fix.
        // This is a likely ocurrance when the system has just been powered up,
        // since the GPS receiver may not have had enough time to aquire a fix.
        // Start the GPS expiration timer. The interval "PROP_GPS_EXPIRATION" should
        // be at least long enough to allow the GPS receiver to make a valid
        // aquisition after a cold-start.
        this.gpsStaleTimer = DateTime.getTimerSec();
        // If a valid fix is not aquired within the expiration interval, then the
        // GPS receiver will be considered stale.
      }
      else if (DateTime.isTimerExpired(this.gpsStaleTimer, gpsExpireInterval)) {
        // We've never received a valid GPS fix, and now the timer has expired.
        // Likely causes: (most likely to least likely)
        // 1) Device restarted while GPS antenna is obstructed (garage, building, etc.)
        // 2) GPS antenna was never attached.
        // 3) GPS receiver was never attached.
        // 4) GPS receiver serial port was improperly specified.
        // The last 2 can be ruled out by checking to see if we've received anything
        // at all from the GPS receiver, even an invalid (type 'V') record.
        GPSReceiver.setGpsStale(true);
      }
      // is GPS fix now considered "stale"?
      if (GPSReceiver.isGpsStale()) {
        // GPS fix expired, now "stale"
        // Client needs to decide what to do in this case
        // Possible actions:
        // 1) Queue up a ERROR_GPS_EXPIRED error
        Log.setMessage(0, "No GPS (stale)");
        Log.debug(LOG_NAME, "****** GPS fix is expired ... ******");
        // ('protocol.c' now sends this error if GPS fix is stale - see 'gpsIsFixStale')
      }
      else {
        // not yet stale
        Log.setMessage(0, "No GPS");
      }
    }
    else

    /* still stale */
    {
      // GPS fix is still stale.
      Log.setMessage(0, "No GPS (stale)");
      // Log.debug(LOG_NAME, "****** GPS fix is expired ... ******\n");
    }

  }

  /**
   * Starts main loop.
   * 
   * @param argv this arguments not used.
   */

  public static void main(String argv[]) {

    /* start main loop */
    Main.getInstance();

  }

}
